package com.googlecode.connectlet.crypto.digest;

import java.io.Serializable;

import com.googlecode.connectlet.util.Bytes;

class MD5State implements Cloneable, Serializable {
	private static final long serialVersionUID = 1L;

	private static final int IV_AA = 0x67452301;
	private static final int IV_BB = 0xefcdab89;
	private static final int IV_CC = 0x98badcfe;
	private static final int IV_DD = 0x10325476;

	private static int ff(int a, int b, int c, int d, int m, int s, int t) {
		return b + Integer.rotateLeft(a + (((c ^ d) & b) ^ d) + m + t, s);
	}

	private static int gg(int a, int b, int c, int d, int m, int s, int t) {
		return b + Integer.rotateLeft(a + (((b ^ c) & d) ^ c) + m + t, s);
	}

	private static int hh(int a, int b, int c, int d, int m, int s, int t) {
		return b + Integer.rotateLeft(a + (b ^ c ^ d) + m + t, s);
	}

	private static int ii(int a, int b, int c, int d, int m, int s, int t) {
		return b + Integer.rotateLeft(a + ((~d | b) ^ c) + m + t, s);
	}

	private int aa = IV_AA, bb = IV_BB, cc = IV_CC, dd = IV_DD;

	MD5State() {/**/}

	MD5State(MD5State o) {
		aa = o.aa;
		bb = o.bb;
		cc = o.cc;
		dd = o.dd;
	}

	void reset() {
		aa = IV_AA;
		bb = IV_BB;
		cc = IV_CC;
		dd = IV_DD;
	}

	void update(byte[] buffer) {
		int a = aa, b = bb, c = cc, d = dd;
		int m[] = new int[16];
		for (int i = 0; i < 16; i ++) {
			m[i] = Bytes.toInt(buffer, i * 4, Bytes.LITTLE_ENDIAN);
		}

		a = ff(a, b, c, d, m[0], 7, 0xd76aa478);
		d = ff(d, a, b, c, m[1], 12, 0xe8c7b756);
		c = ff(c, d, a, b, m[2], 17, 0x242070db);
		b = ff(b, c, d, a, m[3], 22, 0xc1bdceee);
		a = ff(a, b, c, d, m[4], 7, 0xf57c0faf);
		d = ff(d, a, b, c, m[5], 12, 0x4787c62a);
		c = ff(c, d, a, b, m[6], 17, 0xa8304613);
		b = ff(b, c, d, a, m[7], 22, 0xfd469501);
		a = ff(a, b, c, d, m[8], 7, 0x698098d8);
		d = ff(d, a, b, c, m[9], 12, 0x8b44f7af);
		c = ff(c, d, a, b, m[10], 17, 0xffff5bb1);
		b = ff(b, c, d, a, m[11], 22, 0x895cd7be);
		a = ff(a, b, c, d, m[12], 7, 0x6b901122);
		d = ff(d, a, b, c, m[13], 12, 0xfd987193);
		c = ff(c, d, a, b, m[14], 17, 0xa679438e);
		b = ff(b, c, d, a, m[15], 22, 0x49b40821);

		a = gg(a, b, c, d, m[1], 5, 0xf61e2562);
		d = gg(d, a, b, c, m[6], 9, 0xc040b340);
		c = gg(c, d, a, b, m[11], 14, 0x265e5a51);
		b = gg(b, c, d, a, m[0], 20, 0xe9b6c7aa);
		a = gg(a, b, c, d, m[5], 5, 0xd62f105d);
		d = gg(d, a, b, c, m[10], 9, 0x2441453);
		c = gg(c, d, a, b, m[15], 14, 0xd8a1e681);
		b = gg(b, c, d, a, m[4], 20, 0xe7d3fbc8);
		a = gg(a, b, c, d, m[9], 5, 0x21e1cde6);
		d = gg(d, a, b, c, m[14], 9, 0xc33707d6);
		c = gg(c, d, a, b, m[3], 14, 0xf4d50d87);
		b = gg(b, c, d, a, m[8], 20, 0x455a14ed);
		a = gg(a, b, c, d, m[13], 5, 0xa9e3e905);
		d = gg(d, a, b, c, m[2], 9, 0xfcefa3f8);
		c = gg(c, d, a, b, m[7], 14, 0x676f02d9);
		b = gg(b, c, d, a, m[12], 20, 0x8d2a4c8a);

		a = hh(a, b, c, d, m[5], 4, 0xfffa3942);
		d = hh(d, a, b, c, m[8], 11, 0x8771f681);
		c = hh(c, d, a, b, m[11], 16, 0x6d9d6122);
		b = hh(b, c, d, a, m[14], 23, 0xfde5380c);
		a = hh(a, b, c, d, m[1], 4, 0xa4beea44);
		d = hh(d, a, b, c, m[4], 11, 0x4bdecfa9);
		c = hh(c, d, a, b, m[7], 16, 0xf6bb4b60);
		b = hh(b, c, d, a, m[10], 23, 0xbebfbc70);
		a = hh(a, b, c, d, m[13], 4, 0x289b7ec6);
		d = hh(d, a, b, c, m[0], 11, 0xeaa127fa);
		c = hh(c, d, a, b, m[3], 16, 0xd4ef3085);
		b = hh(b, c, d, a, m[6], 23, 0x4881d05);
		a = hh(a, b, c, d, m[9], 4, 0xd9d4d039);
		d = hh(d, a, b, c, m[12], 11, 0xe6db99e5);
		c = hh(c, d, a, b, m[15], 16, 0x1fa27cf8);
		b = hh(b, c, d, a, m[2], 23, 0xc4ac5665);

		a = ii(a, b, c, d, m[0], 6, 0xf4292244);
		d = ii(d, a, b, c, m[7], 10, 0x432aff97);
		c = ii(c, d, a, b, m[14], 15, 0xab9423a7);
		b = ii(b, c, d, a, m[5], 21, 0xfc93a039);
		a = ii(a, b, c, d, m[12], 6, 0x655b59c3);
		d = ii(d, a, b, c, m[3], 10, 0x8f0ccc92);
		c = ii(c, d, a, b, m[10], 15, 0xffeff47d);
		b = ii(b, c, d, a, m[1], 21, 0x85845dd1);
		a = ii(a, b, c, d, m[8], 6, 0x6fa87e4f);
		d = ii(d, a, b, c, m[15], 10, 0xfe2ce6e0);
		c = ii(c, d, a, b, m[6], 15, 0xa3014314);
		b = ii(b, c, d, a, m[13], 21, 0x4e0811a1);
		a = ii(a, b, c, d, m[4], 6, 0xf7537e82);
		d = ii(d, a, b, c, m[11], 10, 0xbd3af235);
		c = ii(c, d, a, b, m[2], 15, 0x2ad7d2bb);
		b = ii(b, c, d, a, m[9], 21, 0xeb86d391);

		aa += a;
		bb += b;
		cc += c;
		dd += d;
	}

	byte[] doFinal() {
		byte[] out = new byte[16];
		Bytes.setInt(aa, out, 0, Bytes.LITTLE_ENDIAN);
		Bytes.setInt(bb, out, 4, Bytes.LITTLE_ENDIAN);
		Bytes.setInt(cc, out, 8, Bytes.LITTLE_ENDIAN);
		Bytes.setInt(dd, out, 12, Bytes.LITTLE_ENDIAN);
		return out;
	}
}

public class MD5Stream extends DigestStream {
	private static final long serialVersionUID = 1L;

	private MD5State state = new MD5State();
	private byte[] buffer = new byte[64];
	private long bytesProcessed = 0;

	@Override
	public void reset() {
		state.reset();
		bytesProcessed = 0;
	}

	@Override
	public void write(byte[] b, int off, int len) {
		int offset = off;
		int length = len;
		int nBlockOffset = ((int) bytesProcessed & 0x3f);
		bytesProcessed += length;
		if (nBlockOffset + length < 64) {
			System.arraycopy(b, offset, buffer, nBlockOffset, length);
		} else {
			int bytesRemain = 64 - nBlockOffset;
			System.arraycopy(b, offset, buffer, nBlockOffset, bytesRemain);
			offset += bytesRemain;
			length -= bytesRemain;
			state.update(buffer);
			while (length >= 64) {
				System.arraycopy(b, offset, buffer, 0, 64);
				offset += 64;
				length -= 64;
				state.update(buffer);
			}
			System.arraycopy(b, offset, buffer, 0, length);
		}
	}

	@Override
	public byte[] digest() {
		MD5State stateFinal = new MD5State(state);
		int nBlockOffset = ((int) bytesProcessed & 0x3f);
		buffer[nBlockOffset] = (byte) 0x80;
		if (nBlockOffset < 56) {
			for (int i = nBlockOffset + 1; i < 56; i ++) {
				buffer[i] = 0;
			}
			Bytes.setLong(bytesProcessed << 3, buffer, 56, Bytes.LITTLE_ENDIAN);
			stateFinal.update(buffer);
			return stateFinal.doFinal();
		}
		for (int i = nBlockOffset + 1; i < 64; i ++) {
			buffer[i] = 0;
		}
		stateFinal.update(buffer);

		byte[] bufferFinal = new byte[64];
		for (int i = 0; i < 56; i ++) {
			bufferFinal[i] = 0;
		}
		Bytes.setLong(bytesProcessed << 3, bufferFinal, 56, Bytes.LITTLE_ENDIAN);
		stateFinal.update(bufferFinal);
		return stateFinal.doFinal();
	}
}